package io.github.mengfly.kvstore;

import io.github.mengfly.kvstore.util.LockUtil;
import lombok.NonNull;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author wangp
 */
public class KvStoreFile implements Closeable {

    private final File file;

    private RandomAccessFile raf;
    private int changeCount = 0;

    private final int MERGE_CHANGE_COUNT = 256;

    private final Map<String, Long> keyPosMap = new ConcurrentHashMap<>();

    private final Map<String, String> cache = new LinkedHashMap<String, String>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
            return size() > 256;
        }
    };

    /**
     * 读写锁
     */
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();


    public KvStoreFile(File file) throws Exception {
        this.file = file;
        if (!file.exists()) {
            if (!file.createNewFile()) {
                throw new IOException("File " + file + " create failed");
            }
        }
        initializeReaderWriter();
    }

    private void initializeReaderWriter() throws Exception {
        close();
        raf = new RandomAccessFile(file, "rw");

        raf.seek(raf.length());
        LockUtil.onLock(lock.readLock(), () -> {
            keyPosMap.clear();
            cache.clear();
            try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
                keyPosMap.putAll(KvEntry.readKeyPosMap(raf));
            }
        });
    }

    public void put(@NonNull String key, @NonNull String value) throws Exception {
        KvEntry entry = new KvEntry(key, value);
        LockUtil.onLock(lock.writeLock(), () -> {
            final long l = writeEntry(entry);
            boolean isUpdate = keyPosMap.containsKey(key);
            keyPosMap.put(entry.getKey(), l);
            cache.put(key, value);
            if (isUpdate) {
                incrementChangeCountAndCheckMerge();
            }
        });
    }

    public String get(@NonNull String key) throws Exception {
        return LockUtil.onLock(lock.readLock(), () -> {
            final Long pos = keyPosMap.get(key);
            if (pos == null) {
                return null;
            }
            if (cache.containsKey(key)) {
                return cache.get(key);
            }
            final String value = get(pos).getValue();
            cache.put(key, value);
            return value;
        });
    }

    public KvEntry get(long position) throws Exception {
        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
            return KvEntry.read(raf, position);
        }
    }

    public boolean del(String key) throws Exception {
        return LockUtil.onLock(lock.writeLock(), () -> {
            // check key exists
            if (!keyPosMap.containsKey(key)) {
                return false;
            }
            KvEntry entry = new KvEntry(key, null, (byte) -1);
            writeEntry(entry);
            keyPosMap.remove(entry.getKey());
            cache.remove(key);
            incrementChangeCountAndCheckMerge();
            return true;
        });

    }

    private void incrementChangeCountAndCheckMerge() throws Exception {
        changeCount++;
        if (changeCount >= MERGE_CHANGE_COUNT) {
            changeCount = 0;
            merge();
        }
    }


    @Override
    public void close() throws IOException {
        if (raf != null) {
            raf.close();
            raf = null;
        }
    }

    private long writeEntry(KvEntry entry) throws IOException {
        // get current position
        final long position = raf.getFilePointer();
        entry.write(raf);
        return position;
    }

    public void merge() throws Exception {

        final Path tempFile = Files.createTempFile("merge-", System.currentTimeMillis() + ".tmp");

        LockUtil.onLock(lock.readLock(), () -> {
            try (RandomAccessFile raf = new RandomAccessFile(tempFile.toFile(), "rws")) {
                keyPosMap.values().forEach(pos -> {
                    try {
                        get(pos).write(raf);
                    } catch (Exception ignored) {
                    }
                });
            }
        });

        // 移除原来的文件
        LockUtil.onLock(lock.writeLock(), () -> {
            close();
            if (file.delete()) {
                Files.move(tempFile, file.toPath());
            } else {
                Files.delete(tempFile);
                throw new IOException("Could not delete file " + file);
            }
            initializeReaderWriter();
        });
    }

    public List<String> keys() {
        return new ArrayList<>(keyPosMap.keySet());
    }

    public int size() {
        return keyPosMap.size();
    }
}
